﻿using System;
using System.Runtime.InteropServices;
using Ast.Core.WinApi;
using Ast.Framework;
using Castle.Core.Logging;

namespace Ast.Core.Services
{
    public interface IDeviceController : IDependency
    {
        bool Send(uint ioControlCode);
        bool Send<TIn>(uint ioControlCode, TIn inData);
        bool Send<TOut>(uint ioControlCode, out TOut outData);
        bool Send<TIn, TOut>(uint ioControlCode, TIn inData, out TOut outData);
        bool Send(uint ioControlCode, IntPtr inDataBuffer, uint inDataSize, IntPtr outDataBuffer, uint outDataSize, ref uint returnDataSize);
    }

    public class DeviceControler : IDeviceController
    {
        private readonly IDeviceProvider _deviceProvider;

        public DeviceControler(IDeviceProvider deviceProvider)
        {
            _deviceProvider = deviceProvider;

            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public bool Send(uint ioControlCode)
        {
            object _;
            return Send(ioControlCode, default(object), out _);
        }

        public bool Send<TIn>(uint ioControlCode, TIn inData)
        {
            object _;
            return Send(ioControlCode, inData, out _);
        }

        public bool Send<TOut>(uint ioControlCode, out TOut outData)
        {
            return Send(ioControlCode, default(object), out outData);
        }

        public bool Send<TIn, TOut>(uint ioControlCode, TIn inData, out TOut outData)
        {
            outData = default(TOut);

            var inDataSize = 0;
            var inDataBuffer = IntPtr.Zero;
            if (typeof (TIn).IsValueType)
            {
                inDataSize = Marshal.SizeOf<TIn>();
                inDataBuffer = Marshal.AllocHGlobal(inDataSize);
                Marshal.StructureToPtr(inData, inDataBuffer, true);
            }

            var outDataSize = 0;
            var outDataBuffer = IntPtr.Zero;
            if (typeof (TOut).IsValueType)
            {
                outDataSize = Marshal.SizeOf<TOut>();
                outDataBuffer = Marshal.AllocHGlobal(outDataSize);
            }
            else if (typeof (TIn) == typeof (IoCtl.ReadMemoryRequest) && (typeof (TOut) == typeof (string) || typeof (TOut) == typeof (byte[])))
            {
                var readMemoryRequest = (IoCtl.ReadMemoryRequest) (object) inData;
                outDataSize = readMemoryRequest.Length;
                outDataBuffer = Marshal.AllocHGlobal(outDataSize);
            }

            var returnDataSize = default(uint);
            var isOk = Send(ioControlCode, inDataBuffer, (uint) inDataSize, outDataBuffer, (uint) outDataSize, ref returnDataSize);
            if (!isOk)
            {
                Logger.Win32ErrorFormat("DeviceIoControl error occurred");
            }

            if (inDataSize > 0)
            {
                Marshal.FreeHGlobal(inDataBuffer);
            }
            if (outDataSize > 0)
            {
                Logger.DebugFormat("IoControl code 0x{0:X}, return data size {1}", ioControlCode, returnDataSize);
                if (isOk)
                {
                    if (typeof (TOut) == typeof (string))
                    {
                        outData = (TOut) (object) Marshal.PtrToStringAuto(outDataBuffer, outDataSize/2);
                    }
                    else if (typeof (TOut) == typeof (byte[]))
                    {
                        var buffer = new byte[outDataSize];
                        Marshal.Copy(outDataBuffer, buffer, 0, outDataSize);
                        outData = (TOut) (object) buffer;
                    }
                    else if (returnDataSize == outDataSize)
                    {
                        outData = Marshal.PtrToStructure<TOut>(outDataBuffer);
                    }
                    else
                    {
                        Logger.ErrorFormat("return data size doesn't match, expecting {0}, but {1}", outDataSize, returnDataSize);
                        isOk = false;
                    }
                }
                Marshal.FreeHGlobal(outDataBuffer);
            }

            return isOk;
        }

        public bool Send(uint ioControlCode, IntPtr inDataBuffer, uint inDataSize, IntPtr outDataBuffer, uint outDataSize, ref uint returnDataSize)
        {
            var isOk = Native.DeviceIoControl(
                _deviceProvider.Device.DangerousGetHandle(), ioControlCode,
                inDataBuffer, inDataSize,
                outDataBuffer, outDataSize,
                ref returnDataSize, IntPtr.Zero
                );
            if (!isOk)
            {
                Logger.Win32ErrorFormat("DeviceIoControl error occurred");
            }
            return isOk;
        }
    }
}